/*******************************************************************************
 * Copyright 2019-2020 Microchip FPGA Embedded Systems Solutions.
 *
 * SPDX-License-Identifier: MIT
 *
 * PoalrFire SoC Microprocessor Subsystem RTC bare metal driver implementation.
 */
#include <string.h>
#include "mpfs_hal/mss_hal.h"
#include "mss_rtc_regs.h"
#include "mss_rtc.h"

#ifdef __cplusplus
extern "C" {
#endif

/*-------------------------------------------------------------------------*//**
 * CONTROL_REG register masks.
 */
#define CONTROL_RUNNING_MASK        0x00000001u
#define CONTROL_RTC_START_MASK      0x00000001u
#define CONTROL_RTC_STOP_MASK       0x00000002u
#define CONTROL_ALARM_ON_MASK       0x00000004u
#define CONTROL_ALARM_OFF_MASK      0x00000008u
#define CONTROL_RESET_MASK          0x00000010u
#define CONTROL_UPLOAD_MASK         0x00000020u
#define CONTROL_WAKEUP_CLR_MASK     0x00000100u
#define CONTROL_UPDATED_MASK        0x00000400u

/*-------------------------------------------------------------------------*//**
 * MODE_REG register masks.
 */
#define MODE_CLK_MODE_MASK          0x00000001u
#define MODE_WAKEUP_EN_MASK         0x00000002u
#define MODE_WAKEUP_RESET_MASK      0x00000004u
#define MODE_WAKEUP_CONTINUE_MASK   0x00000008u

/*-------------------------------------------------------------------------*//**
 * Other masks.
 */
#define MAX_BINARY_HIGHER_COUNT     0x7FFu
#define MASK_32_BIT                 0xFFFFFFFFu
#define MAX_PRESCALAR_COUNT         0x03FFFFFFu
#define CALENDAR_SHIFT              8u

/*-------------------------------------------------------------------------*//**
 * Index into look-up table.
 */
#define SECONDS     0
#define MINUTES     1
#define HOURS       2
#define DAYS        3
#define MONTHS      4
#define YEARS       5
#define WEEKDAYS    6
#define WEEKS       7

/*-------------------------------------------------------------------------*//**
 * A pointer to the RTC_TypeDef structure is used to configure the user selected
 * RTC. This pointer is used by all the mss RTC driver function to carry out the
 * required functionality.
 */
RTC_TypeDef *      mss_rtc;

static uint8_t
get_clock_mode
(
    void
);

static void
set_rtc_mode
(
    uint8_t requested_mode
);

static void add_alarm_cfg_values
(
    uint8_t calendar_item,
    uint32_t * p_calendar_value,
    uint32_t * p_compare_mask
);

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_init
(
    RTC_TypeDef *base_address,
    uint8_t mode,
    uint32_t prescaler
)
{
    ASSERT(prescaler <= MAX_PRESCALAR_COUNT);

    /* Assigning the user selected MSS RTC base address to global RTC structure
     * pointer so that the other driver functions can use it.
     */
    mss_rtc = base_address;

    if (prescaler <= MAX_PRESCALAR_COUNT)
    {
        /* Stop the RTC. */
        MSS_RTC_stop();
        
        /* Disable alarm. */
        mss_rtc->CONTROL_REG = CONTROL_ALARM_OFF_MASK;

        /* Disable Interrupt */
        MSS_RTC_disable_irq();

        /* Clear RTC wake up interrupt signal */
        MSS_RTC_clear_irq();

        /* Select mode of operation, including the wake configuration. */
        if (MSS_RTC_CALENDAR_MODE == mode)
        {
            mss_rtc->MODE_REG = MODE_CLK_MODE_MASK;
        }
        else
        {
            mss_rtc->MODE_REG = 0u;
        }
        
        /* Reset the alarm and compare registers to a known value. */
        mss_rtc->ALARM_LOWER_REG = 0u;
        mss_rtc->ALARM_UPPER_REG = 0u;
        mss_rtc->COMPARE_LOWER_REG = 0u;
        mss_rtc->COMPARE_UPPER_REG = 0u;

        /* Reset the calendar counters */
        MSS_RTC_reset_counter();

        /* Set new Prescaler value */
        mss_rtc->PRESCALER_REG = prescaler;
    }
}

/*-------------------------------------------------------------------------*//**
  See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_set_calendar_count
(
    const mss_rtc_calender_t *new_rtc_value
)
{
    uint8_t error = 0u;
    uint8_t clock_mode;
    
    const uint8_t g_rtc_max_count_lut[] =
    {
       /* Calendar mode */
        59u,    /* Seconds */
        59u,    /* Minutes */
        23u,    /* Hours   */
        31u,    /* Days    */
        12u,    /* Months  */
        254u,   /* Years   */
        7u,     /* Weekdays */
        52u     /* Week    */
    };

    const uint8_t g_rtc_min_count_lut[] =
    {
       /* Calendar mode */
        0u, /* Seconds */
        0u, /* Minutes */
        0u, /* Hours   */
        1u, /* Days    */
        1u, /* Months  */
        0u, /* Years   */
        1u, /* Weekdays */
        1u  /* Week    */
    };
    
    /* Assert if the values cross the limit */
    ASSERT(new_rtc_value->second >= g_rtc_min_count_lut[SECONDS]);
    ASSERT(new_rtc_value->second <= g_rtc_max_count_lut[SECONDS]);
    ASSERT(new_rtc_value->minute >= g_rtc_min_count_lut[MINUTES]);
    ASSERT(new_rtc_value->minute <= g_rtc_max_count_lut[MINUTES]);
    ASSERT(new_rtc_value->hour >= g_rtc_min_count_lut[HOURS]);
    ASSERT(new_rtc_value->hour <= g_rtc_max_count_lut[HOURS]);
    ASSERT(new_rtc_value->day >= g_rtc_min_count_lut[DAYS]);
    ASSERT(new_rtc_value->day <= g_rtc_max_count_lut[DAYS]);
    ASSERT(new_rtc_value->month >= g_rtc_min_count_lut[MONTHS]);
    ASSERT(new_rtc_value->month <= g_rtc_max_count_lut[MONTHS]);
    ASSERT(new_rtc_value->year >= g_rtc_min_count_lut[YEARS]);
    ASSERT(new_rtc_value->year <= g_rtc_max_count_lut[YEARS]);
    ASSERT(new_rtc_value->weekday >= g_rtc_min_count_lut[WEEKDAYS]);
    ASSERT(new_rtc_value->weekday <= g_rtc_max_count_lut[WEEKDAYS]);
    ASSERT(new_rtc_value->week >= g_rtc_min_count_lut[WEEKS]);
    ASSERT(new_rtc_value->week <= g_rtc_max_count_lut[WEEKS]);

    if (new_rtc_value->second < g_rtc_min_count_lut[SECONDS]) {error = 1u;}
    if (new_rtc_value->second > g_rtc_max_count_lut[SECONDS]) {error = 1u;}
    if (new_rtc_value->minute < g_rtc_min_count_lut[MINUTES]) {error = 1u;}
    if (new_rtc_value->minute > g_rtc_max_count_lut[MINUTES]) {error = 1u;}
    if (new_rtc_value->hour < g_rtc_min_count_lut[HOURS]) {error = 1u;}
    if (new_rtc_value->hour > g_rtc_max_count_lut[HOURS]) {error = 1u;}
    if (new_rtc_value->day < g_rtc_min_count_lut[DAYS]) {error = 1u;}
    if (new_rtc_value->day > g_rtc_max_count_lut[DAYS]) {error = 1u;}
    if (new_rtc_value->month < g_rtc_min_count_lut[MONTHS]) {error = 1u;}
    if (new_rtc_value->month > g_rtc_max_count_lut[MONTHS]) {error = 1u;}
    if (new_rtc_value->year < g_rtc_min_count_lut[YEARS]) {error = 1u;}
    if (new_rtc_value->year > g_rtc_max_count_lut[YEARS]) {error = 1u;}
    if (new_rtc_value->weekday < g_rtc_min_count_lut[WEEKDAYS]) {error = 1u;}
    if (new_rtc_value->weekday > g_rtc_max_count_lut[WEEKDAYS]) {error = 1u;}
    if (new_rtc_value->week < g_rtc_min_count_lut[WEEKS]) {error = 1u;}
    if (new_rtc_value->week > g_rtc_max_count_lut[WEEKS]) {error = 1u;}

    /* This function can only be used when the RTC is configured to operate in
     * calendar counter mode. */
    clock_mode = get_clock_mode();
    ASSERT(MSS_RTC_CALENDAR_MODE == clock_mode);
    
    if ((0u == error) && (MSS_RTC_CALENDAR_MODE == clock_mode))
    {
        uint32_t upload_in_progress;
        
       /* Write the RTC new value. */
        mss_rtc->SECONDS_REG = new_rtc_value->second;
        mss_rtc->MINUTES_REG = new_rtc_value->minute;
        mss_rtc->HOURS_REG = new_rtc_value->hour;
        mss_rtc->DAY_REG = new_rtc_value->day;
        mss_rtc->MONTH_REG = new_rtc_value->month;
        mss_rtc->YEAR_REG = new_rtc_value->year;
        mss_rtc->WEEKDAY_REG = new_rtc_value->weekday;
        mss_rtc->WEEK_REG = new_rtc_value->week;

        /* Data is copied, now issue upload command */
        mss_rtc->CONTROL_REG = CONTROL_UPLOAD_MASK ;
        
        /* Wait for the upload to complete. */
        do {
            upload_in_progress = mss_rtc->CONTROL_REG & CONTROL_UPLOAD_MASK;
        } while (upload_in_progress);
    }
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_set_binary_count
(
    uint64_t new_rtc_value
)
{
    uint8_t clock_mode;

    /* This function can only be used when the RTC is configured to operate in
     * binary counter mode. */
    clock_mode = get_clock_mode();
    ASSERT(MSS_RTC_BINARY_MODE == clock_mode);
    
    if (MSS_RTC_BINARY_MODE == clock_mode)
    {
        uint32_t rtc_upper_32_bit_value;

        rtc_upper_32_bit_value = (uint32_t)(new_rtc_value >> 32u) & MASK_32_BIT;

        /* Assert if the values cross the limit */
        ASSERT(rtc_upper_32_bit_value <= MAX_BINARY_HIGHER_COUNT);

        if (rtc_upper_32_bit_value <= MAX_BINARY_HIGHER_COUNT)
        {
            uint32_t upload_in_progress;

            /* Write the RTC new value. */
            mss_rtc->DATE_TIME_LOWER_REG = (uint32_t)new_rtc_value;
            mss_rtc->DATE_TIME_UPPER_REG =
                    (uint32_t)(( new_rtc_value >> 32u) & MAX_BINARY_HIGHER_COUNT);

            /* Data is copied, now issue upload command */
            mss_rtc->CONTROL_REG = CONTROL_UPLOAD_MASK;
            
            /* Wait for the upload to complete. */
            do {
                upload_in_progress = mss_rtc->CONTROL_REG & CONTROL_UPLOAD_MASK;
            } while (upload_in_progress);
        }
    }
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_get_calendar_count
(
    mss_rtc_calender_t *p_rtc_calendar
)
{
    uint8_t clock_mode;
    /* This function can only be used when the RTC is configured to operate in
     * calendar counter mode. */
    clock_mode = get_clock_mode();
    ASSERT(MSS_RTC_CALENDAR_MODE == clock_mode);
    
    if (MSS_RTC_CALENDAR_MODE == clock_mode)
    {
        p_rtc_calendar->second = (uint8_t)mss_rtc->SECONDS_REG;
        p_rtc_calendar->minute = (uint8_t)mss_rtc->MINUTES_REG;
        p_rtc_calendar->hour = (uint8_t)mss_rtc->HOURS_REG;
        p_rtc_calendar->day = (uint8_t)mss_rtc->DAY_REG;
        p_rtc_calendar->month = (uint8_t)mss_rtc->MONTH_REG;
        p_rtc_calendar->year = (uint8_t)mss_rtc->YEAR_REG;
        p_rtc_calendar->weekday = (uint8_t)mss_rtc->WEEKDAY_REG;
        p_rtc_calendar->week = (uint8_t)mss_rtc->WEEK_REG;
    }
    else
    {
        /* Set returned calendar count to zero if the RTC is not configured for
         * calendar counter mode. This should make incorrect release application
         * code behave consistently and help application debugging. */
        memset(p_rtc_calendar, 0, sizeof(mss_rtc_calender_t));
    }
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
uint64_t
MSS_RTC_get_binary_count
(
    void
)
{
    uint64_t rtc_count;
    uint8_t clock_mode;

    /* This function can only be used when the RTC is configured to operate in
     * binary counter mode. */
    clock_mode = get_clock_mode();
    ASSERT(MSS_RTC_BINARY_MODE == clock_mode);

    if (MSS_RTC_BINARY_MODE == clock_mode)
    {
        rtc_count = mss_rtc->DATE_TIME_LOWER_REG;
        rtc_count = rtc_count | ((uint64_t)mss_rtc->DATE_TIME_UPPER_REG << 32u);
    }
    else
    {
        /* Set returned binary count to zero if the RTC is not configured for
         * binary counter mode. This should make incorrect release application
         * code behave consistently and help application debugging. */
        rtc_count = 0u;
    }

    return rtc_count;
}

static void add_alarm_cfg_values
(
    uint8_t calendar_item,
    uint32_t * p_calendar_value,
    uint32_t * p_compare_mask    
)
{
    if (MSS_RTC_CALENDAR_DONT_CARE == calendar_item)
    {
        *p_calendar_value = (uint32_t)(*p_calendar_value << CALENDAR_SHIFT);
        *p_compare_mask = (uint32_t)(*p_compare_mask << CALENDAR_SHIFT);
    }
    else
    {
        *p_calendar_value = (uint32_t)((*p_calendar_value << CALENDAR_SHIFT) | 
                                       (uint32_t)calendar_item);
        *p_compare_mask = (uint32_t)((*p_compare_mask << CALENDAR_SHIFT) | 
                                     (uint32_t)0xFFu);
    }
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void MSS_RTC_set_calendar_count_alarm
(
    const mss_rtc_calender_t * alarm_value
)
{
    uint32_t calendar_value;
    uint32_t compare_mask;
    uint8_t mode;
    
    mode = (uint8_t)(mss_rtc->MODE_REG & MODE_CLK_MODE_MASK);
    
    /* This function can only be used with the RTC set to operate in calendar
     * mode. */
    ASSERT(MSS_RTC_CALENDAR_MODE == mode);

    if (MSS_RTC_CALENDAR_MODE == mode)
    {
        uint8_t required_mode_reg;
        
        /* Disable the alarm before updating */
        mss_rtc->CONTROL_REG = CONTROL_ALARM_OFF_MASK;

        /* Set alarm and compare lower registers. */
        calendar_value = 0u;
        compare_mask = 0u;
        
        add_alarm_cfg_values(alarm_value->day, &calendar_value, &compare_mask);
        add_alarm_cfg_values(alarm_value->hour, &calendar_value, &compare_mask);
        add_alarm_cfg_values(alarm_value->minute, &calendar_value, &compare_mask);
        add_alarm_cfg_values(alarm_value->second, &calendar_value, &compare_mask);

        mss_rtc->ALARM_LOWER_REG = calendar_value;
        mss_rtc->COMPARE_LOWER_REG = compare_mask;

        /* Set alarm and compare upper registers. */
        calendar_value = 0u;
        compare_mask = 0u;

        add_alarm_cfg_values(alarm_value->week, &calendar_value, &compare_mask);
        add_alarm_cfg_values(alarm_value->weekday, &calendar_value, &compare_mask);
        add_alarm_cfg_values(alarm_value->year, &calendar_value, &compare_mask);
        add_alarm_cfg_values(alarm_value->month, &calendar_value, &compare_mask);

        mss_rtc->ALARM_UPPER_REG = calendar_value;
        mss_rtc->COMPARE_UPPER_REG = compare_mask;
        
        /* Configure the RTC to enable the alarm. */
        required_mode_reg = mode | MODE_WAKEUP_EN_MASK | MODE_WAKEUP_CONTINUE_MASK;
        set_rtc_mode(required_mode_reg);
        
        /* Enable the alarm */
        mss_rtc->CONTROL_REG = CONTROL_ALARM_ON_MASK ;
    }
}

/*-------------------------------------------------------------------------*//**
  We only write the RTC mode register if really required because the RTC needs
  to be stopped for the mode register to be written. Stopping the RTC every time
  the wake-up alarm configuration is set might induce drift on the RTC time.
  This function is intended to be used when setting alarms.
 */
static void
set_rtc_mode
(
    uint8_t requested_mode
)
{
    if (mss_rtc->MODE_REG != requested_mode)
    {
        uint32_t rtc_running;
        rtc_running = mss_rtc->CONTROL_REG & CONTROL_RUNNING_MASK;
        if (rtc_running)
        {
            /* Stop the RTC in order to change the mode register content. */
            MSS_RTC_stop();
            mss_rtc->MODE_REG = requested_mode;
            MSS_RTC_start();
        }
        else
        {
            mss_rtc->MODE_REG = requested_mode;
        }
    }
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_set_binary_count_alarm
(
    uint64_t alarm_value,
    mss_rtc_alarm_type_t alarm_type
)
{
    uint8_t mode;
    
    mode = (uint8_t)(mss_rtc->MODE_REG & MODE_CLK_MODE_MASK);

    /* This function can only be used with the RTC set to operate in binary
     * counter mode. */
    ASSERT(MSS_RTC_BINARY_MODE == mode);
    if (MSS_RTC_BINARY_MODE == mode)
    {
        uint8_t required_mode_reg;
        
        /* Disable the alarm before updating */
        mss_rtc->CONTROL_REG = CONTROL_ALARM_OFF_MASK;

        /* Set the alarm value. */
        mss_rtc->COMPARE_LOWER_REG = COMPARE_ALL_BITS;
        mss_rtc->COMPARE_UPPER_REG = COMPARE_ALL_BITS;
        mss_rtc->ALARM_LOWER_REG = (uint32_t)alarm_value;
        mss_rtc->ALARM_UPPER_REG = (uint32_t)(alarm_value >> 32u);

        /* Configure the RTC to enable the alarm. */
        required_mode_reg = mode | MODE_WAKEUP_EN_MASK | MODE_WAKEUP_CONTINUE_MASK;
        if (MSS_RTC_PERIODIC_ALARM == alarm_type)
        {
            /* The RTC binary counter will be fully reset when the alarm occurs.
             * The counter will continue counting while the wake-up interrupt is
             * active. */
            required_mode_reg |= MODE_WAKEUP_RESET_MASK;
        }
        set_rtc_mode(required_mode_reg);

        /* Enable the alarm */
        mss_rtc->CONTROL_REG = CONTROL_ALARM_ON_MASK;

        uint8_t test = get_clock_mode();
    }
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_start
(
    void
)
{
    mss_rtc->CONTROL_REG = CONTROL_RTC_START_MASK;
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_stop 
(
    void
)
{
    uint32_t rtc_running;
    
    /* Send command to stop RTC. */
    mss_rtc->CONTROL_REG = CONTROL_RTC_STOP_MASK;
    
    /* Wait for RTC internal synchronization to take place and RTC to actually
     * stop. */
    do {
        rtc_running =  mss_rtc->CONTROL_REG & CONTROL_RUNNING_MASK;
    } while(rtc_running);
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_reset_counter
(
    void
)
{
    uint32_t upload_in_progress;
    
    mss_rtc->CONTROL_REG = CONTROL_RESET_MASK;
    
    /* Wait for the upload to complete. */
    do {
        upload_in_progress = mss_rtc->CONTROL_REG & CONTROL_UPLOAD_MASK;
    } while (upload_in_progress);
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
uint32_t
MSS_RTC_get_update_flag
(
    void
)
{
    uint32_t updated;
    updated = mss_rtc->CONTROL_REG & CONTROL_UPDATED_MASK;
    return updated;
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void 
MSS_RTC_clear_update_flag
(
    void
)
{
    /* Clear the "updated" control bit. */
    mss_rtc->CONTROL_REG = CONTROL_UPDATED_MASK;
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_enable_irq
(
    void
)
{
    /* Only the PLIC level interrupt enable is performed within this function.
     * The RTC level interrupt enable is performed within the alarm setting
     * functions.
     * This avoid the MODE register being modified whenever  RTC
     * interrupts are enabled/disabled. */
    PLIC_EnableIRQ(RTC_WAKEUP_PLIC);
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_disable_irq
(
    void
)
{
    /* Only the PLIC level interrupt disable is performed within this function.
     * This avoid the MODE register being modified whenever RTC
     * interrupts are enabled/disabled. */
    PLIC_DisableIRQ(RTC_WAKEUP_PLIC);
}

/*-------------------------------------------------------------------------*//**
 * See "mss_rtc.h" for details of how to use this function.
 */
void
MSS_RTC_clear_irq
(
    void
)
{
    volatile uint32_t dummy_read;
    
    /* Clear wake up interrupt signal */
    mss_rtc->CONTROL_REG = CONTROL_WAKEUP_CLR_MASK;
    
    /* Ensure that the posted write to the CONTROL_REG register completed before
     * returning from this function. Not doing this may result in the interrupt
     * only being cleared some time after this function returns. */
    dummy_read = mss_rtc->CONTROL_REG;
    
    /* Dummy operation to avoid warning message */
    ++dummy_read;
}

/*-------------------------------------------------------------------------*//**
  The get_clock_mode() function gets the clock mode of RTC hardware.
  Possible clock modes are:
    MSS_RTC_CALENDAR_MODE
    MSS_RTC_BINARY_MODE
 */
static uint8_t
get_clock_mode
(
    void
)
{
    uint8_t clock_mode;

    clock_mode = (uint8_t)(mss_rtc->MODE_REG & MODE_CLK_MODE_MASK);

    return(clock_mode);
}

#ifdef __cplusplus
}
#endif
